package nl.cypherpunk.statefuzzer.fuzzing;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.io.IOUtils;

import java.io.*;
import java.util.*;

public class MutateMessage {
    public static Random rand;
    public Integer newSize;
    public byte[] newMessage;
    public Integer maxSize;
    public Integer mutateSize;
    static Integer[] interesting_8={-128,-1,0,1,16,32,64,100,127};
    static Integer ARITH_MAX = 35;
    public MutateMessage(Random rand){
        this.rand = rand;
        this.maxSize = 4096;
        this.mutateSize = 10;
        this.newSize = 0;
    }

    public MutateMessage(){
        this.maxSize = 4096;
        this.mutateSize = 10;
        this.newSize = 0;
    }

    public void MutateRadamsa(byte[] message, Integer size, String seedPool){
        this.maxSize = 4096;
        this.mutateSize = 8;
        this.newSize = 0;
        byte [] tmp = new byte[size];
        newMessage = new byte[4096];
        System.arraycopy(message,0,tmp,0,size);
        try{
            String cmd = "radamsa -o - ./"+seedPool+"/*";
            String comArray[] = new String[]{"bash", "-c",cmd};

            ProcessBuilder pb = new ProcessBuilder(comArray);
            pb.redirectErrorStream(true);

            Process process = pb.start();
            InputStream stdout = process.getInputStream();
            //IOUtils.write(tmp, stdin);
            // process.waitFor();
            this.newSize = IOUtils.read(stdout,newMessage);
            process.destroy();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    public void MutateDispatcher(byte[] message, Integer size)
    {
        int i = rand.nextInt(this.mutateSize);
        boolean flag = false;
        switch (i)
        {
            case 0:
                flag = MutateChangeBit(message, size);
                break;
            case 1:
                flag = setInterestingValue(message,size);
                break;
            case 2:
                flag = addToByte(message,size);
                break;
            case 3:
                flag = MutateShuffleBytes(message,size);
                break;
            case 4:
                flag = MutateEraseBytes(message,size);
                break;
            case 5:
                flag = MutateInsertByte(message,size);
                break;
            case 6:
                flag = MutateInsertRandomBytes(message,size);
                break;
            case 7:
                flag = MutateSwapByte(message,size);
                break;
            case 8:
                flag = MutateSwapBytes(message,size);
                break;
            default:
                flag = MutateChangeByte(message,size);
                break;
        }
        if(flag == false)
        {
            newSize = size;
            newMessage = message;
        }
    }


    public boolean setInterestingValue(byte[] seed, Integer size){
        seed[rand.nextInt(size)] = interesting_8[rand.nextInt(interesting_8.length)].byteValue();
        return true;
    }

    public boolean addToByte(byte[] seed, Integer size){
        seed[rand.nextInt(size)] += 1 + rand.nextInt(ARITH_MAX);
        return true;
    }

    public boolean MutateShuffleBytes(byte[] message, Integer size)
    {
        if(size < 8 || size > maxSize) return false;
        int ShuffleAmount = rand.nextInt(Math.min(size,8));
        int ShuffleStart = rand.nextInt(size-ShuffleAmount);
        List<Byte> list = new ArrayList<Byte>(Arrays.asList(ArrayUtils.toObject(message)));
        Collections.shuffle(list.subList(ShuffleStart,ShuffleStart+ShuffleAmount),rand);
        newSize = size;
        newMessage = ArrayUtils.toPrimitive(list.toArray(new Byte[newSize]));
        return true;
    }

    public boolean MutateEraseBytes(byte[] message, Integer size)
    {
        if(size <= 1) return false;
        int N = rand.nextInt(size/2) + 1;
        int idx = rand.nextInt(size - N + 1);
        List<Byte> list = new ArrayList<Byte>(Arrays.asList(ArrayUtils.toObject(message)));
        List<Byte> tmp = list.subList(0,idx);
        tmp.addAll(list.subList(idx+N,size));
        newSize = tmp.size();
        newMessage = ArrayUtils.toPrimitive(tmp.toArray(new Byte[newSize]));
        return true;
    }

    public boolean MutateInsertByte(byte[] message, Integer size)
    {
        if(size > maxSize) return false;
        int idx = rand.nextInt(size);
        List<Byte> list = new ArrayList<Byte>(Arrays.asList(ArrayUtils.toObject(message)));
        List<Byte> tmp = list.subList(0,idx);
        byte[] randByte = new byte[1];
        rand.nextBytes(randByte);
        tmp.add(randByte[0]);
        tmp.addAll(list.subList(idx+1,size+1));
        newSize = size + 1;
        newMessage = ArrayUtils.toPrimitive(tmp.toArray(new Byte[newSize]));
        return true;
    }

    public boolean MutateInsertRandomBytes(byte[] message, Integer size)
    {
        int minBytesToInsert = 3;
        if(size + minBytesToInsert >= maxSize) return false;
        int maxBytesToInsert = rand.nextInt( maxSize-size);
        int N = rand.nextInt(Math.max(maxBytesToInsert-minBytesToInsert,1))+minBytesToInsert;
        int idx = rand.nextInt(size);
        List<Byte> list = new ArrayList<Byte>(Arrays.asList(ArrayUtils.toObject(message)));
        List<Byte> tmp = list.subList(0,idx);
        for(int i = 0; i < N; i++)
        {
            byte[] randByte = new byte[1];
            rand.nextBytes(randByte);
            tmp.add(randByte[0]);
        }
        tmp.addAll(list.subList(idx+N,size+N));
        newSize = size + N;
        newMessage = ArrayUtils.toPrimitive(tmp.toArray(new Byte[newSize]));
        return true;
    }

    public boolean MutateChangeByte(byte[] message, Integer size)
    {
        if(size > maxSize) return false;
        int idx = rand.nextInt(size);
        newMessage = message;
        byte[] randByte = new byte[1];
        rand.nextBytes(randByte);
        newMessage[idx] = randByte[0];
        newSize = size;
        return true;
    }

    public boolean MutateChangeBit(byte[] message, Integer size)
    {
        if(size > maxSize) return false;
        int idx = rand.nextInt(size);
        newMessage = message;
        newMessage[idx] ^= 1 << rand.nextInt(8);
        newSize = size;
        return true;
    }

    public boolean MutateSwapByte(byte[] message, Integer size)
    {
        if(size > maxSize) return false;
        int idx1 = rand.nextInt(size);
        int idx2 = rand.nextInt(size);
        newMessage = message;
        newMessage[idx1] = message[idx2];
        newMessage[idx2] = message[idx1];
        newSize = size;
        return true;
    }

    public boolean MutateSwapBytes(byte[] message, Integer size)
    {
        if(size > maxSize) return false;
        int idx1 = rand.nextInt(size);
        int length = rand.nextInt(size - idx1);
        int idx2 = rand.nextInt(size-length);
        newMessage = message;
        for(int i = 0; i < length; i++)
        {
            newMessage[idx1+i] = message[idx2+i];
            newMessage[idx2+i] = message[idx1+i];
        }
        newSize = size;
        return true;
    }
}
